numpy高级教程(四)——内存映射文件与性能建议
python进阶教程
机器学习
深度学习
长按二维码关注
进入正文
numpy高级教程(四)
声明:numpy的系列文章终于要先告一段落了,本篇为系列文章的第四篇,简单介绍一下numpy的内存映射文件处理方式以及我们在处理大规模数组的时候要注意的性能问题。
目录
一 内存映射简介
1.1 numpy的memmap简介
1.2 memmap的简单实践
1.3 HDF5文件的简要说明
二 性能建议
2.1 python数据处理的注意细节
2.2 连续内存的重要性
01
numpy内存映射简介
np.save和np.load可用于读写磁盘上以二进制格式存储的数组。其实还有一些工具可用于更为复杂的场景。尤其是内存映像(memorymap),它使你能处理在内存中放不下的数据集。
class numpy.memmap
为存储在磁盘上的二进制文件中的数组创建内存映射。内存映射文件用于访问磁盘上的很大的数据文件,而无需将整个文件读入内存。NumPy的memmap是类似于数组的对象。 这与Python的mmap模块(这是python本身用于处理内存映射文件的方法)不同,后者使用类似文件的对象。
此类可能在某些时候被转换为工厂函数,该函数将视图返回到mmap缓冲区。
删除memmap实例以关闭memmap文件。
参数:
filename:str,类文件对象或pathlib.Path实例
要用作数组数据缓冲区的文件名或文件对象。
dtype:数据类型,可选用于解释文件内容的数据类型。 默认是uint8。
mode:{'r +','r','w +','c'},可选
该文件以此模式打开:
'r'打开现有文件以供阅读。
'r +'打开现有文件进行读写。
'w +'创建或覆盖现有文件以进行读写。
'c'Copy-on-write:赋值会影响内存中的数据,但更改不会保存到磁盘。 磁盘上的文件是只读的。 默认为'r +'。
offset:int,可选
在该文件中,数组数据从此偏移量开始。 由于偏移量是以字节为单位测量的,因此通常应该是dtype字节大小的倍数。 当模式!='r'时,甚至超出文件末尾的正偏移也是有效的; 该文件将被扩展以容纳附加数据。 默认情况下,memmap将从文件的开头开始,即使filename是文件指针fp和fp.tell()!= 0。
shape:tuple,可选
所需的阵列形状。 如果mode =='r'并且offset之后的剩余字节数不是dtype的字节大小的倍数,则必须指定shape。 默认情况下,返回的数组将是1-D,其元素数由文件大小和数据类型确定。
order:{'C','F'},可选
指定ndarray内存布局的顺序:row-major,C-style或column-major,
Fortran-style。 这仅在形状大于1-D时有效。 默认顺序为“C”。
See also:lib.format.open_memmap
创建或加载内存映射的.npy文件。
Notes
memmap对象可以在接受ndarray的任何地方使用。 给定一个memmapfp,isinstance(fp,numpy.ndarray)返回True。
在32位系统上,内存映射文件不能大于2GB。
当memmap导致文件系统中创建或扩展文件超出其当前大小时,新部件的内容未指定。 在具有POSIX文件系统语义的系统上,扩展部分将填充零字节。
下面看几个小例子
>>> data = np.arange(12, dtype='float32')
>>> data.resize((3,4))
此示例使用临时文件,以便doctest不会将文件写入您的目录。 您将使用“正常”文件名。
>> from tempfile import mkdtemp
>>> import os.path as path
>>> filename = path.join(mkdtemp(), 'newfile.dat')
创建一个与我们的数据匹配的dtype和形状的memmap:
>> from tempfile import mkdtemp
>>> import os.path as path
>>> filename = path.join(mkdtemp(), 'newfile.dat')
将数据写入memmap数组:
>>> fp[:] = data[:]
>>> fp
memmap([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]], dtype=float32)
>>> fp.filename == path.abspath(filename)
True
在删除对象之前,删除会将内存更改刷新到磁盘:
>>> del fp
加载memmap并验证数据是否已存储:
>>> newfp = np.memmap(filename, dtype='float32', mode='r', shape=(3,4))
>>> newfp
memmap([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]], dtype=float32)
只读memmap:
>>> fpr = np.memmap(filename, dtype='float32', mode='r', shape=(3,4))
>>> fpr.flags.writeable
False
写时复制memmap:
>>> fpc = np.memmap(filename, dtype='float32', mode='c', shape=(3,4))
>>> fpc.flags.writeable
True
可以分配给写时复制数组,但是值只写入数组的内存副本,而不是写入磁盘:
>>> fpc
memmap([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]], dtype=float32)
>>> fpc[0,:] = 0
>>> fpc
memmap([[ 0., 0., 0., 0.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]], dtype=float32)
磁盘上的文件未更改:
>>> fpr
memmap([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]], dtype=float32)
偏移到memmap:
>>> fpo = np.memmap(filename, dtype='float32', mode='r', offset=16)
>>> fpo
memmap([ 4., 5., 6., 7., 8., 9., 10., 11.], dtype=float32)
Attributes:filename:str or pathlib.Path instance
映射文件的路径。
offset:int
文件中的偏移位置。
mode:str
文件模式。
Methods:
flush()将数组中的任何更改写入磁盘上的文件。
前面说了,内存映像文件是一种将磁盘上的非常大的二进制数据文件当做内存中的数组进行处理的方式。NumPy实现了一个类似于ndarray的memmap对象,它允许将大文件分成小段进行读写,而不是一次性将整个数组读入内存。另外,memmap也拥有跟普通数组一样的方法,因此,基本上只要是能用于ndarray的算法就也能用于memmap。
要创建一个内存映像,可以使用函数np.memmap并传入一个文件路径、数据类型、形状以及文件模式:
In [214]: mmap = np.memmap('mymmap', dtype='float64', mode='w+',
.....: shape=(10000, 10000))
In [215]: mmap
Out[215]:
memmap([[ 0., 0., 0., ..., 0., 0., 0.],
[ 0., 0., 0., ..., 0., 0., 0.],
[ 0., 0., 0., ..., 0., 0., 0.],
...,
[ 0., 0., 0., ..., 0., 0., 0.],
[ 0., 0., 0., ..., 0., 0., 0.],
[ 0., 0., 0., ..., 0., 0., 0.]])
对memmap切片将会返回磁盘上的数据的视图:
In [216]: section = mmap[:5]
如果将数据赋值给这些视图:数据会先被缓存在内存中(就像是Python的文件对象),调用flush即可将其写入磁盘:
In [217]: section[:] = np.random.randn(5, 10000)
In [218]: mmap.flush()
In [219]: mmap
Out[219]:
memmap([[ 0.7584, -0.6605, 0.8626, ..., 0.6046, -0.6212, 2.0542],
[-1.2113, -1.0375, 0.7093, ..., -1.4117, -0.1719, -0.8957],
[-0.1419, -0.3375, 0.4329, ..., 1.2914, -0.752 , -0.44 ],
...,
[ 0. , 0. , 0. , ..., 0. , 0. , 0. ],
[ 0. , 0. , 0. , ..., 0. , 0. , 0. ],
[ 0. , 0. , 0. , ..., 0. , 0. , 0. ]])
In [220]: del mmap
只要某个内存映像超出了作用域,它就会被垃圾回收器回收,之前对其所做的任何修改都会被写入磁盘。当打开一个已经存在的内存映像时,仍然需要指明数据类型和形状,因为磁盘上的那个文件只是一块二进制数据而已,没有任何元数据:
In [221]: mmap = np.memmap('mymmap', dtype='float64', shape=(10000,
10000))
In [222]: mmap
Out[222]:
memmap([[ 0.7584, -0.6605, 0.8626, ..., 0.6046, -0.6212, 2.0542],
[-1.2113, -1.0375, 0.7093, ..., -1.4117, -0.1719, -0.8957],
[-0.1419, -0.3375, 0.4329, ..., 1.2914, -0.752 , -0.44 ],
...,
[ 0. , 0. , 0. , ..., 0. , 0. , 0. ],
[ 0. , 0. , 0. , ..., 0. , 0. , 0. ],
[ 0. , 0. , 0. , ..., 0. , 0. , 0. ]])
内存映像可以使用前面介绍的结构化或嵌套dtype。
HDF5及其他数组存储方式
PyTables和h5py这两个Python项目可以将NumPy的数组数据存储为高效且可压缩的HDF5格式(HDF意思是“层次化数据式”)。你可以安全地将好几百GB甚至TB的数据存储为HDF5格式。想要详细了解HDF5的小伙伴可以查阅相关的技术文档。
02
性能建议
python数组处理的注意事项
使用NumPy的代码的性能一般都很不错,因为数组运算一般都比纯Python循环快得多。下面大致列出了一些需要注意的事项:
●将Python循环和条件逻辑转换为数组运算和布尔数组运算。
●尽量使用广播。
●避免复制数据,尽量使用数组视图(即片)。
●利用ufunc及其各种方法。
如果单用NumPy无论如何都达不到所需的性能指标,就可以考虑一下用C、Fortran或Cython(等下会稍微介绍一下)来编写代码,我自己在工作中经常会用到Cython(http://cython.org),因为它不用花费我太多精力就能得到C语言那样的性能。
连续内存的重要性
虽然这个话题有点超出本书的范围,但还是要提一下,因为在某些应用场景中,数组的内存布局可以对计算速度造成极大的影响。这是因为性能差别在一定程度上跟CPU的高速缓存(cache)体系有关。运算过程中访问连续内存块(例如,对以C顺序存储的数组的行求和)一般是最快的,因为内存子系统会将适当的内存块缓存到超高速的L1或L2CPU Cache中。此外,NumPy的C语言基础代码(某些)对连续存储的情况进行了优化处理,这样就能避免一些跨越式的内存访问。
一个数组的内存布局是连续的,就是说元素是以它们在数组中出现的顺序(即Fortran型(列优先)或C型(行优先))存储在内存中的。默认情况下,NumPy
数组是以C型连续的方式创建的。列优先的数组(比如C型连续数组的转置)也被称为Fortran型连续。通过ndarray的flags属性即可查看这些信息:
In [225]: arr_c = np.ones((1000, 1000), order='C')
In [226]: arr_f = np.ones((1000, 1000), order='F')
In [227]: arr_c.flags
Out[227]:
C_CONTIGUOUS : True
F_CONTIGUOUS : False
OWNDATA : True
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False
In [228]: arr_f.flags
Out[228]:
C_CONTIGUOUS : False
F_CONTIGUOUS : True
OWNDATA : True
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False
In [229]: arr_f.flags.f_contiguous
Out[229]: True
在这个例子中,对两个数组的行进行求和计算,理论上说,arr_c会比arr_f快,因为arr_c的行在内存中是连续的。我们可以在IPython中用%timeit来确认一下:
In [230]: %timeit arr_c.sum(1)
784 us +- 10.4 us per loop (mean +- std. dev. of 7 runs, 1000 loops
each)
In [231]: %timeit arr_f.sum(1)
934 us +- 29 us per loop (mean +- std. dev. of 7 runs, 1000 loops
each)
如果想从NumPy中提升性能,这里就应该是下手的地方。如果数组的内存顺序不符合你的要求,使用copy并传入'C'或'F'即可解决该问题:
In [232]: arr_f.copy('C').flags
Out[232]:
C_CONTIGUOUS : True
F_CONTIGUOUS : False
OWNDATA : True
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False
注意,在构造数组的视图时,其结果不一定是连续的:
In [233]: arr_c[:50].flags.contiguous
Out[233]: True
In [234]: arr_c[:, :50].flags
Out[234]:
C_CONTIGUOUS : False
F_CONTIGUOUS : False
OWNDATA : False
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False
我们一起过双旦
2018.12.28
Best Wishes to You
最后,再次感谢一路陪伴的小伙伴,希望2018年每个人都能够收获满满,新的2019年都能够赚的盆满锅满!
猜您喜欢往期精选▼3. numpy高级教程(一)——高级数组操作与广播broadcast
4. numpy高级教程(二)——通用函数ufunc与结构化数组structured array
5. python标准库系列教程(一)——itertools
6. python标准库系列教程(二)——functools (上篇)
7.Python高级编程——描述符Descriptor超详细讲解(补充篇之底层原理实现)
8.Python高级编程——描述符Descriptor超详细讲解(中篇之属性控制)
赶紧关注我们吧
您的点赞和分享是我们进步的动力!
↘↘↘